Software projects often rely on external code libraries, known as dependencies. Package managers, such as Swift Package Manager, allow developers
to reference dependencies for their projects. These dependencies simplify development, but also introduce risk as they download and include external
code based on a project’s configuration. When adding a dependency, developers often specify a range of acceptable versions, such as any version
beginning with 1.2. When the project is built, the package manager performs dependency resolution. This process involves checking the
repository and selecting one specific version that satisfies the version range, which becomes the resolved dependency.
Dependency resolution becomes a security concern when the specific resolved version is not recorded or "locked." Without a file containing the
locked version, the resolution process runs every time the project is built on a new system. The core issue is that the package manager will always
query the repository to find the most recent version available, rendering the application vulnerable when malicious versions of dependencies are
released.
This is often a key component of what is called a "supply chain attack." The attacker isn’t directly attacking your application. Instead, they are
attacking a component you use. This is an important consideration because the attack’s source is less obvious. You might diligently secure your own
code, but overlook the risk introduced by external dependencies.
Ask Yourself Whether
- Your team or company has the security policy to enforce predictability of dependency versions at build time.
There is a risk if you answer yes to this question.
Recommended Secure Coding Practices
Swift Package Manager automatically creates a lock file named Package.resolved. You should commit this file to your source code
repository.
Sensitive Code Example
Your project’s Package.swift file might define a dependency with a version range.
let package = Package(
name: "Example",
dependencies: [
.package(url: "https://example.com/library.git", from: "1.2.3"),
],
targets: [
.target(
name: "Example",
dependencies: ["library"]),
]
)
If a Package.resolved file is absent in the project, there is no guarantee that continuous integration systems and other developers on
your team will use the exact same version of the dependency.
Compliant Solution
After you build the project for the first time, Swift Package Manager will resolve the dependencies and create a Package.resolved
file. This file contains a reference to the specific commit hash of the version that was downloaded.
let package = Package(
name: "Example",
dependencies: [
.package(url: "https://example.com/library.git", from: "1.2.3"),
],
targets: [
.target(
name: "Example",
dependencies: ["library"]),
]
)
{
"object": {
"pins": [
{
"package": "library",
"repositoryURL": "https://example.com/library.git",
"state": {
"branch": null,
"revision": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
"version": "1.2.5"
}
}
]
},
"version": 1
}
Run swift package update when you need to update a dependency to a newer version. This command finds the latest versions allowed by
the ranges in your Package.swift file and updates the Package.resolved file accordingly. After running the command, review
the changes to Package.resolved and then commit the updated file to your repository. This makes the update a deliberate and traceable
action.
See